library(tidyverse)
library(tidytext)
library(lubridate)

source("prep.R")
df <- read_csv("outputs/facts.csv") %>%
  mutate(year = year(date))

ingredients <- read_csv("outputs/ingredients.csv")

Yearly numbers

Let’s see how the number of recipes have been by year.

df %>%
  count(year) %>%
  arrange(year)
df %>%
  count(year) %>%
  ggplot(aes(year, n)) +
  geom_line()

df %>%
  count(year) %>%
  mutate(n = cumsum(n)) %>%
  ggplot(aes(year, n)) +
  geom_line()

There weren’t too many before 2004, a huge spike in 2004, and then a slow taper afterwards. They probably did some backend retooling which re-dated all the articles before 2004 to 2004. It is strange that the number of articles has been steadily decreasing.

Let’s also look at the ratings. Based on what we just saw, we will focus only after 2004.

df %>%
  filter(year > 2004) %>%
  group_by(year) %>%
  summarize(rating = mean(rating, na.rm = TRUE)) %>%
  ggplot(aes(year, rating)) +
  geom_line()

Well the average rating has been going up, so it seems like Epicurious has been focusing on quality over quantity.

There was a serious spike in 2021, which is interesting. The data was scraped in November, so there was enough data that it probably wasn’t just chance. Perhaps this was pandemic related?

Authors

Who are the most prolific authors?

unnest_authors <- df %>%
  unnest_tokens(author, author, token = str_split, pattern = ";")

unnest_authors %>%
  filter(!is.na(author)) %>%
  count(author) %>%
  arrange(-n)

Bunch of Bon Appetit people, not surprising given that Conde Nast owns both. Let’s look at when they were prolific.

df %>%
  filter(!is.na(author)) %>%
  add_count(author) %>%
  filter(n >= 169) %>% # get top 10
  count(author, year) %>%
  ggplot(aes(year, n)) +
  geom_line() +
  facet_wrap(~author, scales = "free_y")

This all makes sense. BA published as a team until around 2016, which is also when some of the BA people started showing up. They were probably pushing the personalities more. Also note that a lot of the BA people stopped showing up around last year, which was when the blow up happened.

We also look at the most prolific authors by year.

unnest_authors %>%
  filter(year > 2004, !is.na(author)) %>%
  count(year, author) %>%
  group_by(year) %>%
  slice_max(n, n = 5)

Ingredients

We now analyze the ingredients of recipes. We first expand the table by each ingredient in the recipe.

ingr <- df %>%
  left_join(ingredients, by = "id") %>%
  prep(to_mat = FALSE)

ingr

Base is the cleaned ingredient name. Let’s count how times each ingredients have shown up. What are the most popular ingredients?

ingr %>%
  count(base) %>%
  arrange(-n) %>%
  head(100)

What is the distribution of the number of ingredients per recipe like?

ingr %>%
  count(id) %>%
  ggplot(aes(n)) +
  geom_histogram()
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

We will look at how ingredients change under two divisions: time and rating. Let’s first look at time. We will split time into two parts: early <= 2004 (due to the timestamp in the data) and late >= 2014 (when Epicurious and BA were combined in a digital platform). We want to calculate log odds, so we will ignore the inbetween.

log_odds <- function(df, field, a, b) {
  df %>%
    count(base, {{ field }}) %>%
    group_by({{ field }}) %>%
    mutate(prob = (n + 1) / (sum(n) + 1)) %>%
    pivot_wider(base, names_from = {{ field }}, values_from = prob, values_fill = 0) %>%
    filter({{ a }} > 0, {{ b }} > 0) %>%
    mutate(log_odds = log({{ a }}, base = 2) - log({{ b }}, base = 2))
}
ingr %>%
  mutate(period = case_when(
    year <= 2004 ~ "Early",
    year >= 2014 ~ "Late",
    TRUE ~ "Middle"
  )) %>%
  filter(period != "Middle") %>%
  log_odds(period, Late, Early) %>%
  {
    \(x) bind_rows(slice_max(., log_odds, n = 10), slice_min(., log_odds, n = 10))
  }() %>%
  mutate(base = fct_reorder(base, log_odds)) %>%
  ggplot(aes(log_odds, base)) +
  geom_col(aes(fill = log_odds > 0)) +
  theme(legend.position = "none") +
  labs(x = "Log odds of appearing in later recipes", y = "Ingredient")

We also look at the difference between the top and bottom quartiles. Recipes of the bottom quartile tend to feature basic ingredients like pastas and legumes whereas the top rated recipes have more exotic ingredients like ramps, cotija, and grappa.

quantile(df$rating, probs = c(0.25, 0.75), na.rm = TRUE)
 25%  75% 
3.00 3.66 
ingr %>%
  mutate(group = case_when(
    rating <= 3 ~ "Lo",
    rating >= 3.66 ~ "Hi",
    TRUE ~ "Middle"
  )) %>%
  filter(group != "Middle") %>%
  log_odds(group, Hi, Lo) %>%
  {
    \(x) bind_rows(slice_max(., log_odds, n = 10), slice_min(., log_odds, n = 10))
  }() %>%
  mutate(base = fct_reorder(base, log_odds)) %>%
  ggplot(aes(log_odds, base)) +
  geom_col(aes(fill = log_odds > 0)) +
  theme(legend.position = "none") +
  labs(x = "Log odds of appearing in highly rated recipes", y = "Ingredient")

Similarities

We now look at the similarity matrix that we will use in the recommender.

source("ingredients-rec/ingr.rec.R")

sim <- ingr %>%
  prep(min_num = 10) %>%
  create_sim()

We will pivot the similarity table into long form.

sims <- sim$sim %>%
  as_tibble() %>%
  mutate(ingr1 = colnames(.)) %>%
  pivot_longer(!ingr1, names_to = "ingr2", values_to = "val") %>%
  filter(ingr1 < ingr2)

sims

Let’s look at a histogram of the similarities.

sims %>%
  ggplot(aes(val)) +
  stat_ecdf()

Over 80% of pairs have no similarity value and most values are under 0.2.

We now look at a graph where we connect ingredients with particularly high similarty.

library(igraph)
library(ggraph)

set.seed(2022) # reproducibility

sims %>%
  filter(val > 0.22) %>%
  graph_from_data_frame() %>%
  ggraph(layout = "fr") +
  geom_edge_link() +
  geom_node_point() +
  geom_node_text(aes(label = name), size = 3, vjust = 1, hjust = 1)

We see some natural connected components like baking, asian stir fry, cocktail, and gluten free baking.

Clustering

Finally, we will do clustering of the item vectors. We first create the item vector matrix where the vectors are all unit. This is code from the create_sim function.

mat <- df %>%
  left_join(ingredients, by = "id") %>%
  prep(min_num = 5)

mat <- t(mat / sqrt(rowSums(mat)))
mat <- mat / sqrt(rowSums(mat * mat))

We now run kmeans to cluster the vectors. Note that Euclidean distance between unit vectors corresponds well with the cosine of the angle by law of cosines.

set.seed(2021) # reproducibility

res <- kmeans(mat, centers = 10, nstart = 10)

tibble(
  ingredient = rownames(mat),
  cluster = res$cluster
) %>%
  group_by(cluster) %>%
  slice_head(n = 5) %>%
  print(n = 50)
# A tibble: 50 × 2
# Groups:   cluster [10]
   ingredient              cluster
   <chr>                     <int>
 1 table salt                    1
 2 fine sea salt                 1
 3 avocado oil                   1
 4 medjool date                  1
 5 yeast                         1
 6 chamomile tea bags            2
 7 fruit                         2
 8 creme de cassis               2
 9 grapes                        2
10 mint leaves                   2
11 button mushrooms              3
12 oyster mushrooms              3
13 chiles de arbol               3
14 sherry wine vinegar           3
15 horseradish                   3
16 black peppercorn              4
17 lemongrass                    4
18 plain whole milk yogurt       4
19 xanthan gum                   4
20 coriander                     4
21 angostura bitters             5
22 orange twist                  5
23 light rum                     5
24 vodka                         5
25 ice cube                      5
26 white onion                   6
27 sour cream                    6
28 cilantro leaves               6
29 lime juice                    6
30 avocado                       6
31 honey                         7
32 molasses                      7
33 maple syrup                   7
34 almond                        7
35 chocolate                     7
36 red bell pepper               8
37 olive oil                     8
38 garlic                        8
39 bay leave                     8
40 worcestershire sauce          8
41 salt                          9
42 unsalted butter               9
43 egg                           9
44 sugar                         9
45 vanilla extract               9
46 shiitake mushroom            10
47 vegetable oil                10
48 soy sauce                    10
49 asian sesame oil             10
50 hoisin sauce                 10

Some reasonable clusters. Cluster 5 is obviously cocktails, 7 is probably sweet baked goods, 9 is more generic baked goods, and 10 is Asian cuisine. The other clusters also have themes, but less obvious to interpretation. It seems there is definitely information in the distance between item vectors.

YGBge3IsIGluY2x1ZGUgPSBGQUxTRX0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KHdhcm5pbmcgPSBGQUxTRSkKYGBgCgpgYGB7ciwgbWVzc2FnZT1GQUxTRX0KbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkodGlkeXRleHQpCmxpYnJhcnkobHVicmlkYXRlKQoKc291cmNlKCJwcmVwLlIiKQpgYGAKCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQpkZiA8LSByZWFkX2Nzdigib3V0cHV0cy9mYWN0cy5jc3YiKSAlPiUKICBtdXRhdGUoeWVhciA9IHllYXIoZGF0ZSkpCgppbmdyZWRpZW50cyA8LSByZWFkX2Nzdigib3V0cHV0cy9pbmdyZWRpZW50cy5jc3YiKQpgYGAKCiMjIyBZZWFybHkgbnVtYmVycwoKTGV0J3Mgc2VlIGhvdyB0aGUgbnVtYmVyIG9mIHJlY2lwZXMgaGF2ZSBiZWVuIGJ5IHllYXIuCgpgYGB7cn0KZGYgJT4lCiAgY291bnQoeWVhcikgJT4lCiAgYXJyYW5nZSh5ZWFyKQoKZGYgJT4lCiAgY291bnQoeWVhcikgJT4lCiAgZ2dwbG90KGFlcyh5ZWFyLCBuKSkgKwogIGdlb21fbGluZSgpCgpkZiAlPiUKICBjb3VudCh5ZWFyKSAlPiUKICBtdXRhdGUobiA9IGN1bXN1bShuKSkgJT4lCiAgZ2dwbG90KGFlcyh5ZWFyLCBuKSkgKwogIGdlb21fbGluZSgpCmBgYAoKVGhlcmUgd2VyZW4ndCB0b28gbWFueSBiZWZvcmUgMjAwNCwgYSBodWdlIHNwaWtlIGluIDIwMDQsIGFuZCB0aGVuIGEgc2xvdyB0YXBlciBhZnRlcndhcmRzLiAgVGhleSBwcm9iYWJseSBkaWQgc29tZSBiYWNrZW5kIHJldG9vbGluZyB3aGljaCByZS1kYXRlZCBhbGwgdGhlIGFydGljbGVzIGJlZm9yZSAyMDA0IHRvIDIwMDQuICBJdCBpcyBzdHJhbmdlIHRoYXQgdGhlIG51bWJlciBvZiBhcnRpY2xlcyBoYXMgYmVlbiBzdGVhZGlseSBkZWNyZWFzaW5nLgoKTGV0J3MgYWxzbyBsb29rIGF0IHRoZSByYXRpbmdzLiAgQmFzZWQgb24gd2hhdCB3ZSBqdXN0IHNhdywgd2Ugd2lsbCBmb2N1cyBvbmx5IGFmdGVyIDIwMDQuCgpgYGB7cn0KZGYgJT4lCiAgZmlsdGVyKHllYXIgPiAyMDA0KSAlPiUKICBncm91cF9ieSh5ZWFyKSAlPiUKICBzdW1tYXJpemUocmF0aW5nID0gbWVhbihyYXRpbmcsIG5hLnJtID0gVFJVRSkpICU+JQogIGdncGxvdChhZXMoeWVhciwgcmF0aW5nKSkgKwogIGdlb21fbGluZSgpCmBgYAoKV2VsbCB0aGUgYXZlcmFnZSByYXRpbmcgaGFzIGJlZW4gZ29pbmcgdXAsIHNvIGl0IHNlZW1zIGxpa2UgRXBpY3VyaW91cyBoYXMgYmVlbiBmb2N1c2luZyBvbiBxdWFsaXR5IG92ZXIgcXVhbnRpdHkuCgpUaGVyZSB3YXMgYSBzZXJpb3VzIHNwaWtlIGluIDIwMjEsIHdoaWNoIGlzIGludGVyZXN0aW5nLiAgVGhlIGRhdGEgd2FzIHNjcmFwZWQgaW4gTm92ZW1iZXIsIHNvIHRoZXJlIHdhcyBlbm91Z2ggZGF0YSB0aGF0IGl0IHByb2JhYmx5IHdhc24ndCBqdXN0IGNoYW5jZS4gIFBlcmhhcHMgdGhpcyB3YXMgcGFuZGVtaWMgcmVsYXRlZD8KCiMjIyBBdXRob3JzCgpXaG8gYXJlIHRoZSBtb3N0IHByb2xpZmljIGF1dGhvcnM/CgpgYGB7cn0KdW5uZXN0X2F1dGhvcnMgPC0gZGYgJT4lCiAgdW5uZXN0X3Rva2VucyhhdXRob3IsIGF1dGhvciwgdG9rZW4gPSBzdHJfc3BsaXQsIHBhdHRlcm4gPSAiOyIpCgp1bm5lc3RfYXV0aG9ycyAlPiUKICBmaWx0ZXIoIWlzLm5hKGF1dGhvcikpICU+JQogIGNvdW50KGF1dGhvcikgJT4lCiAgYXJyYW5nZSgtbikKYGBgCgpCdW5jaCBvZiBCb24gQXBwZXRpdCBwZW9wbGUsIG5vdCBzdXJwcmlzaW5nIGdpdmVuIHRoYXQgQ29uZGUgTmFzdCBvd25zIGJvdGguICBMZXQncyBsb29rIGF0IHdoZW4gdGhleSB3ZXJlIHByb2xpZmljLgoKYGBge3J9CmRmICU+JQogIGZpbHRlcighaXMubmEoYXV0aG9yKSkgJT4lCiAgYWRkX2NvdW50KGF1dGhvcikgJT4lCiAgZmlsdGVyKG4gPj0gMTY5KSAlPiUgIyBnZXQgdG9wIDEwCiAgY291bnQoYXV0aG9yLCB5ZWFyKSAlPiUKICBnZ3Bsb3QoYWVzKHllYXIsIG4pKSArCiAgZ2VvbV9saW5lKCkgKwogIGZhY2V0X3dyYXAofmF1dGhvciwgc2NhbGVzID0gImZyZWVfeSIpCmBgYAoKVGhpcyBhbGwgbWFrZXMgc2Vuc2UuICBCQSBwdWJsaXNoZWQgYXMgYSB0ZWFtIHVudGlsIGFyb3VuZCAyMDE2LCB3aGljaCBpcyBhbHNvIHdoZW4gc29tZSBvZiB0aGUgQkEgcGVvcGxlIHN0YXJ0ZWQgc2hvd2luZyB1cC4gIFRoZXkgd2VyZSBwcm9iYWJseSBwdXNoaW5nIHRoZSBwZXJzb25hbGl0aWVzIG1vcmUuICBBbHNvIG5vdGUgdGhhdCBhIGxvdCBvZiB0aGUgQkEgcGVvcGxlIHN0b3BwZWQgc2hvd2luZyB1cCBhcm91bmQgbGFzdCB5ZWFyLCB3aGljaCB3YXMgd2hlbiB0aGUgW2Jsb3cgdXBdKGh0dHBzOi8vd3d3LmVhdGVyLmNvbS8yMDIwLzgvNi8yMTM1NzM0MS9wcml5YS1rcmlzaG5hLXJpY2stbWFydGluZXotc29obGEtZWwtd2F5bGx5LXJlc2lnbi1ib24tYXBwZXRpdC10ZXN0LWtpdGNoZW4tdmlkZW9zKSBoYXBwZW5lZC4KCldlIGFsc28gbG9vayBhdCB0aGUgbW9zdCBwcm9saWZpYyBhdXRob3JzIGJ5IHllYXIuCgpgYGB7cn0KdW5uZXN0X2F1dGhvcnMgJT4lCiAgZmlsdGVyKHllYXIgPiAyMDA0LCAhaXMubmEoYXV0aG9yKSkgJT4lCiAgY291bnQoeWVhciwgYXV0aG9yKSAlPiUKICBncm91cF9ieSh5ZWFyKSAlPiUKICBzbGljZV9tYXgobiwgbiA9IDUpCmBgYAoKIyMjIEluZ3JlZGllbnRzCgpXZSBub3cgYW5hbHl6ZSB0aGUgaW5ncmVkaWVudHMgb2YgcmVjaXBlcy4gIFdlIGZpcnN0IGV4cGFuZCB0aGUgdGFibGUgYnkgZWFjaCBpbmdyZWRpZW50IGluIHRoZSByZWNpcGUuCgpgYGB7cn0KaW5nciA8LSBkZiAlPiUKICBsZWZ0X2pvaW4oaW5ncmVkaWVudHMsIGJ5ID0gImlkIikgJT4lCiAgcHJlcCh0b19tYXQgPSBGQUxTRSkKCmluZ3IKYGBgCgpCYXNlIGlzIHRoZSBjbGVhbmVkIGluZ3JlZGllbnQgbmFtZS4gIExldCdzIGNvdW50IGhvdyB0aW1lcyBlYWNoIGluZ3JlZGllbnRzIGhhdmUgc2hvd24gdXAuICBXaGF0IGFyZSB0aGUgbW9zdCBwb3B1bGFyIGluZ3JlZGllbnRzPwoKYGBge3J9CmluZ3IgJT4lCiAgY291bnQoYmFzZSkgJT4lCiAgYXJyYW5nZSgtbikgJT4lCiAgaGVhZCgxMDApCmBgYAoKV2hhdCBpcyB0aGUgZGlzdHJpYnV0aW9uIG9mIHRoZSBudW1iZXIgb2YgaW5ncmVkaWVudHMgcGVyIHJlY2lwZSBsaWtlPwoKYGBge3J9CmluZ3IgJT4lCiAgY291bnQoaWQpICU+JQogIGdncGxvdChhZXMobikpICsKICBnZW9tX2hpc3RvZ3JhbSgpCmBgYAoKV2Ugd2lsbCBsb29rIGF0IGhvdyBpbmdyZWRpZW50cyBjaGFuZ2UgdW5kZXIgdHdvIGRpdmlzaW9uczogdGltZSBhbmQgcmF0aW5nLiAgTGV0J3MgZmlyc3QgbG9vayBhdCB0aW1lLiAgV2Ugd2lsbCBzcGxpdCB0aW1lIGludG8gdHdvIHBhcnRzOiBlYXJseSA8PSAyMDA0IChkdWUgdG8gdGhlIHRpbWVzdGFtcCBpbiB0aGUgZGF0YSkgYW5kIGxhdGUgPj0gMjAxNCAod2hlbiBFcGljdXJpb3VzIGFuZCBCQSB3ZXJlIGNvbWJpbmVkIGluIGEgZGlnaXRhbCBwbGF0Zm9ybSkuICBXZSB3YW50IHRvIGNhbGN1bGF0ZSBsb2cgb2Rkcywgc28gd2Ugd2lsbCBpZ25vcmUgdGhlIGluYmV0d2Vlbi4KCmBgYHtyfQpsb2dfb2RkcyA8LSBmdW5jdGlvbihkZiwgZmllbGQsIGEsIGIpIHsKICBkZiAlPiUKICAgIGNvdW50KGJhc2UsIHt7IGZpZWxkIH19KSAlPiUKICAgIGdyb3VwX2J5KHt7IGZpZWxkIH19KSAlPiUKICAgIG11dGF0ZShwcm9iID0gKG4gKyAxKSAvIChzdW0obikgKyAxKSkgJT4lCiAgICBwaXZvdF93aWRlcihiYXNlLCBuYW1lc19mcm9tID0ge3sgZmllbGQgfX0sIHZhbHVlc19mcm9tID0gcHJvYiwgdmFsdWVzX2ZpbGwgPSAwKSAlPiUKICAgIGZpbHRlcih7eyBhIH19ID4gMCwge3sgYiB9fSA+IDApICU+JQogICAgbXV0YXRlKGxvZ19vZGRzID0gbG9nKHt7IGEgfX0sIGJhc2UgPSAyKSAtIGxvZyh7eyBiIH19LCBiYXNlID0gMikpCn0KYGBgCgpgYGB7cn0KaW5nciAlPiUKICBtdXRhdGUocGVyaW9kID0gY2FzZV93aGVuKAogICAgeWVhciA8PSAyMDA0IH4gIkVhcmx5IiwKICAgIHllYXIgPj0gMjAxNCB+ICJMYXRlIiwKICAgIFRSVUUgfiAiTWlkZGxlIgogICkpICU+JQogIGZpbHRlcihwZXJpb2QgIT0gIk1pZGRsZSIpICU+JQogIGxvZ19vZGRzKHBlcmlvZCwgTGF0ZSwgRWFybHkpICU+JQogIHsKICAgIFwoeCkgYmluZF9yb3dzKHNsaWNlX21heCguLCBsb2dfb2RkcywgbiA9IDEwKSwgc2xpY2VfbWluKC4sIGxvZ19vZGRzLCBuID0gMTApKQogIH0oKSAlPiUKICBtdXRhdGUoYmFzZSA9IGZjdF9yZW9yZGVyKGJhc2UsIGxvZ19vZGRzKSkgJT4lCiAgZ2dwbG90KGFlcyhsb2dfb2RkcywgYmFzZSkpICsKICBnZW9tX2NvbChhZXMoZmlsbCA9IGxvZ19vZGRzID4gMCkpICsKICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAibm9uZSIpICsKICBsYWJzKHggPSAiTG9nIG9kZHMgb2YgYXBwZWFyaW5nIGluIGxhdGVyIHJlY2lwZXMiLCB5ID0gIkluZ3JlZGllbnQiKQpgYGAKCldlIGFsc28gbG9vayBhdCB0aGUgZGlmZmVyZW5jZSBiZXR3ZWVuIHRoZSB0b3AgYW5kIGJvdHRvbSBxdWFydGlsZXMuICBSZWNpcGVzIG9mIHRoZSBib3R0b20gcXVhcnRpbGUgdGVuZCB0byBmZWF0dXJlIGJhc2ljIGluZ3JlZGllbnRzIGxpa2UgcGFzdGFzIGFuZCBsZWd1bWVzIHdoZXJlYXMgdGhlIHRvcCByYXRlZCByZWNpcGVzIGhhdmUgbW9yZSBleG90aWMgaW5ncmVkaWVudHMgbGlrZSByYW1wcywgY290aWphLCBhbmQgZ3JhcHBhLgoKYGBge3J9CnF1YW50aWxlKGRmJHJhdGluZywgcHJvYnMgPSBjKDAuMjUsIDAuNzUpLCBuYS5ybSA9IFRSVUUpCgppbmdyICU+JQogIG11dGF0ZShncm91cCA9IGNhc2Vfd2hlbigKICAgIHJhdGluZyA8PSAzIH4gIkxvIiwKICAgIHJhdGluZyA+PSAzLjY2IH4gIkhpIiwKICAgIFRSVUUgfiAiTWlkZGxlIgogICkpICU+JQogIGZpbHRlcihncm91cCAhPSAiTWlkZGxlIikgJT4lCiAgbG9nX29kZHMoZ3JvdXAsIEhpLCBMbykgJT4lCiAgewogICAgXCh4KSBiaW5kX3Jvd3Moc2xpY2VfbWF4KC4sIGxvZ19vZGRzLCBuID0gMTApLCBzbGljZV9taW4oLiwgbG9nX29kZHMsIG4gPSAxMCkpCiAgfSgpICU+JQogIG11dGF0ZShiYXNlID0gZmN0X3Jlb3JkZXIoYmFzZSwgbG9nX29kZHMpKSAlPiUKICBnZ3Bsb3QoYWVzKGxvZ19vZGRzLCBiYXNlKSkgKwogIGdlb21fY29sKGFlcyhmaWxsID0gbG9nX29kZHMgPiAwKSkgKwogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikgKwogIGxhYnMoeCA9ICJMb2cgb2RkcyBvZiBhcHBlYXJpbmcgaW4gaGlnaGx5IHJhdGVkIHJlY2lwZXMiLCB5ID0gIkluZ3JlZGllbnQiKQpgYGAKCiMjIyBTaW1pbGFyaXRpZXMKCldlIG5vdyBsb29rIGF0IHRoZSBzaW1pbGFyaXR5IG1hdHJpeCB0aGF0IHdlIHdpbGwgdXNlIGluIHRoZSByZWNvbW1lbmRlci4KCmBgYHtyfQpzb3VyY2UoImluZ3JlZGllbnRzLXJlYy9pbmdyLnJlYy5SIikKCnNpbSA8LSBpbmdyICU+JQogIHByZXAobWluX251bSA9IDEwKSAlPiUKICBjcmVhdGVfc2ltKCkKYGBgCgpXZSB3aWxsIHBpdm90IHRoZSBzaW1pbGFyaXR5IHRhYmxlIGludG8gbG9uZyBmb3JtLgoKYGBge3J9CnNpbXMgPC0gc2ltJHNpbSAlPiUKICBhc190aWJibGUoKSAlPiUKICBtdXRhdGUoaW5ncjEgPSBjb2xuYW1lcyguKSkgJT4lCiAgcGl2b3RfbG9uZ2VyKCFpbmdyMSwgbmFtZXNfdG8gPSAiaW5ncjIiLCB2YWx1ZXNfdG8gPSAidmFsIikgJT4lCiAgZmlsdGVyKGluZ3IxIDwgaW5ncjIpCgpzaW1zCmBgYAoKTGV0J3MgbG9vayBhdCBhIGhpc3RvZ3JhbSBvZiB0aGUgc2ltaWxhcml0aWVzLgoKYGBge3J9CnNpbXMgJT4lCiAgZ2dwbG90KGFlcyh2YWwpKSArCiAgc3RhdF9lY2RmKCkKYGBgCgpPdmVyIDgwJSBvZiBwYWlycyBoYXZlIG5vIHNpbWlsYXJpdHkgdmFsdWUgYW5kIG1vc3QgdmFsdWVzIGFyZSB1bmRlciAwLjIuCgpXZSBub3cgbG9vayBhdCBhIGdyYXBoIHdoZXJlIHdlIGNvbm5lY3QgaW5ncmVkaWVudHMgd2l0aCBwYXJ0aWN1bGFybHkgaGlnaCBzaW1pbGFydHkuCgpgYGB7ciwgbWVzc2FnZT1GQUxTRX0KbGlicmFyeShpZ3JhcGgpCmxpYnJhcnkoZ2dyYXBoKQoKc2V0LnNlZWQoMjAyMikgIyByZXByb2R1Y2liaWxpdHkKCnNpbXMgJT4lCiAgZmlsdGVyKHZhbCA+IDAuMjIpICU+JQogIGdyYXBoX2Zyb21fZGF0YV9mcmFtZSgpICU+JQogIGdncmFwaChsYXlvdXQgPSAiZnIiKSArCiAgZ2VvbV9lZGdlX2xpbmsoKSArCiAgZ2VvbV9ub2RlX3BvaW50KCkgKwogIGdlb21fbm9kZV90ZXh0KGFlcyhsYWJlbCA9IG5hbWUpLCBzaXplID0gMywgdmp1c3QgPSAxLCBoanVzdCA9IDEpCmBgYAoKV2Ugc2VlIHNvbWUgbmF0dXJhbCBjb25uZWN0ZWQgY29tcG9uZW50cyBsaWtlIGJha2luZywgYXNpYW4gc3RpciBmcnksIGNvY2t0YWlsLCBhbmQgZ2x1dGVuIGZyZWUgYmFraW5nLgoKIyMjIENsdXN0ZXJpbmcKCkZpbmFsbHksIHdlIHdpbGwgZG8gY2x1c3RlcmluZyBvZiB0aGUgaXRlbSB2ZWN0b3JzLiAgV2UgZmlyc3QgY3JlYXRlIHRoZSBpdGVtIHZlY3RvciBtYXRyaXggd2hlcmUgdGhlIHZlY3RvcnMgYXJlIGFsbCB1bml0LiAgVGhpcyBpcyBjb2RlIGZyb20gdGhlIGNyZWF0ZV9zaW0gZnVuY3Rpb24uCgpgYGB7cn0KbWF0IDwtIGRmICU+JQogIGxlZnRfam9pbihpbmdyZWRpZW50cywgYnkgPSAiaWQiKSAlPiUKICBwcmVwKG1pbl9udW0gPSA1KQoKbWF0IDwtIHQobWF0IC8gc3FydChyb3dTdW1zKG1hdCkpKQptYXQgPC0gbWF0IC8gc3FydChyb3dTdW1zKG1hdCAqIG1hdCkpCmBgYAoKV2Ugbm93IHJ1biBrbWVhbnMgdG8gY2x1c3RlciB0aGUgdmVjdG9ycy4gIE5vdGUgdGhhdCBFdWNsaWRlYW4gZGlzdGFuY2UgYmV0d2VlbiB1bml0IHZlY3RvcnMgY29ycmVzcG9uZHMgd2VsbCB3aXRoIHRoZSBjb3NpbmUgb2YgdGhlIGFuZ2xlIGJ5IGxhdyBvZiBjb3NpbmVzLgoKYGBge3J9CnNldC5zZWVkKDIwMjEpICMgcmVwcm9kdWNpYmlsaXR5CgpyZXMgPC0ga21lYW5zKG1hdCwgY2VudGVycyA9IDEwLCBuc3RhcnQgPSAxMCkKCnRpYmJsZSgKICBpbmdyZWRpZW50ID0gcm93bmFtZXMobWF0KSwKICBjbHVzdGVyID0gcmVzJGNsdXN0ZXIKKSAlPiUKICBncm91cF9ieShjbHVzdGVyKSAlPiUKICBzbGljZV9oZWFkKG4gPSA1KSAlPiUKICBwcmludChuID0gNTApCmBgYAoKU29tZSByZWFzb25hYmxlIGNsdXN0ZXJzLiAgQ2x1c3RlciA1IGlzIG9idmlvdXNseSBjb2NrdGFpbHMsIDcgaXMgcHJvYmFibHkgc3dlZXQgYmFrZWQgZ29vZHMsIDkgaXMgbW9yZSBnZW5lcmljIGJha2VkIGdvb2RzLCBhbmQgMTAgaXMgQXNpYW4gY3Vpc2luZS4gIFRoZSBvdGhlciBjbHVzdGVycyBhbHNvIGhhdmUgdGhlbWVzLCBidXQgbGVzcyBvYnZpb3VzIHRvIGludGVycHJldGF0aW9uLiAgSXQgc2VlbXMgdGhlcmUgaXMgZGVmaW5pdGVseSBpbmZvcm1hdGlvbiBpbiB0aGUgZGlzdGFuY2UgYmV0d2VlbiBpdGVtIHZlY3RvcnMuCg==